/**
 *  Copyright (c) 2022 Red Hat, Inc. and others.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v2.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v20.html
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 *  Contributors:
 *  Red Hat Inc. - initial API and implementation
 */
package org.eclipse.lemminx.services.format;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.commons.TextDocument;
import org.eclipse.lemminx.dom.DOMAttr;
import org.eclipse.lemminx.dom.DOMCDATASection;
import org.eclipse.lemminx.dom.DOMComment;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.dom.DOMParser;
import org.eclipse.lemminx.dom.DOMProcessingInstruction;
import org.eclipse.lemminx.dom.DOMText;
import org.eclipse.lemminx.dom.DTDAttlistDecl;
import org.eclipse.lemminx.dom.DTDDeclNode;
import org.eclipse.lemminx.dom.DTDDeclParameter;
import org.eclipse.lemminx.services.extensions.format.IFormatterParticipant;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lemminx.settings.XMLFormattingOptions.EmptyElements;
import org.eclipse.lemminx.settings.XMLFormattingOptions.SplitAttributes;
import org.eclipse.lemminx.utils.XMLBuilder;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextEdit;

/**
 * Default XML formatter which generates one text edit by rewriting the DOM node
 * which must be formatted.
 * 
 * @author Angelo ZERR
 *
 */
public class XMLFormatterDocumentOld {
	private final TextDocument textDocument;
	private final Range range;
	private final SharedSettings sharedSettings;
	private final Collection<IFormatterParticipant> formatterParticipants;
	private final EmptyElements emptyElements;

	private int startOffset;
	private int endOffset;
	private DOMDocument fullDomDocument;
	private DOMDocument rangeDomDocument;
	private XMLBuilder xmlBuilder;
	private int indentLevel;
	private boolean linefeedOnNextWrite;
	private boolean withinDTDContent;

	/**
	 * XML formatter document.
	 */
	public XMLFormatterDocumentOld(TextDocument textDocument, Range range, SharedSettings sharedSettings,
			Collection<IFormatterParticipant> formatterParticipants) {
		this.textDocument = textDocument;
		this.range = range;
		this.sharedSettings = sharedSettings;
		this.formatterParticipants = formatterParticipants;
		this.emptyElements = sharedSettings.getFormattingSettings().getEmptyElements();
		this.linefeedOnNextWrite = false;
	}

	/**
	 * Returns a List containing a single TextEdit, containing the newly formatted
	 * changes of this.textDocument
	 *
	 * @return List containing a single TextEdit
	 * @throws BadLocationException
	 */
	public List<? extends TextEdit> format() throws BadLocationException {
		this.fullDomDocument = DOMParser.getInstance().parse(textDocument.getText(), textDocument.getUri(), null,
				false);

		if (isRangeFormatting()) {
			setupRangeFormatting(range);
		} else {
			setupFullFormatting(range);
		}

		this.indentLevel = getStartingIndentLevel();
		format(this.rangeDomDocument);

		List<? extends TextEdit> textEdits = getFormatTextEdit();
		return textEdits;
	}

	private boolean isRangeFormatting() {
		return this.range != null;
	}

	private void setupRangeFormatting(Range range) throws BadLocationException {
		int startOffset = this.textDocument.offsetAt(range.getStart());
		int endOffset = this.textDocument.offsetAt(range.getEnd());

		Position startPosition = this.textDocument.positionAt(startOffset);
		Position endPosition = this.textDocument.positionAt(endOffset);
		enlargePositionToGutters(startPosition, endPosition);

		this.startOffset = this.textDocument.offsetAt(startPosition);
		this.endOffset = this.textDocument.offsetAt(endPosition);

		String fullText = this.textDocument.getText();
		String rangeText = fullText.substring(this.startOffset, this.endOffset);

		withinDTDContent = this.fullDomDocument.isWithinInternalDTD(startOffset);
		String uri = this.textDocument.getUri();
		if (withinDTDContent) {
			uri += ".dtd";
		}
		this.rangeDomDocument = DOMParser.getInstance().parse(rangeText, uri, null, false);

		if (containsTextWithinStartTag()) {
			adjustOffsetToStartTag();
			rangeText = fullText.substring(this.startOffset, this.endOffset);
			this.rangeDomDocument = DOMParser.getInstance().parse(rangeText, uri, null, false);
		}

		this.xmlBuilder = new XMLBuilder(this.sharedSettings, "", textDocument.lineDelimiter(startPosition.getLine()),
				formatterParticipants);
	}

	private boolean containsTextWithinStartTag() {

		if (this.rangeDomDocument.getChildren().size() < 1) {
			return false;
		}

		DOMNode firstChild = this.rangeDomDocument.getChild(0);
		if (!firstChild.isText()) {
			return false;
		}

		int tagContentOffset = firstChild.getStart();
		int fullDocOffset = getFullOffsetFromRangeOffset(tagContentOffset);
		DOMNode fullNode = this.fullDomDocument.findNodeAt(fullDocOffset);

		if (!fullNode.isElement()) {
			return false;
		}
		return ((DOMElement) fullNode).isInStartTag(fullDocOffset);
	}

	private void adjustOffsetToStartTag() throws BadLocationException {
		int tagContentOffset = this.rangeDomDocument.getChild(0).getStart();
		int fullDocOffset = getFullOffsetFromRangeOffset(tagContentOffset);
		DOMNode fullNode = this.fullDomDocument.findNodeAt(fullDocOffset);
		Position nodePosition = this.textDocument.positionAt(fullNode.getStart());
		nodePosition.setCharacter(0);
		this.startOffset = this.textDocument.offsetAt(nodePosition);
	}

	private void setupFullFormatting(Range range) throws BadLocationException {
		this.startOffset = 0;
		this.endOffset = textDocument.getText().length();
		this.rangeDomDocument = this.fullDomDocument;

		Position startPosition = textDocument.positionAt(startOffset);
		this.xmlBuilder = new XMLBuilder(this.sharedSettings, "", textDocument.lineDelimiter(startPosition.getLine()),
				formatterParticipants);
	}

	private void enlargePositionToGutters(Position start, Position end) throws BadLocationException {
		start.setCharacter(0);

		if (end.getCharacter() == 0 && end.getLine() > 0) {
			end.setLine(end.getLine() - 1);
		}

		end.setCharacter(this.textDocument.lineText(end.getLine()).length());
	}

	private int getStartingIndentLevel() throws BadLocationException {
		if (withinDTDContent) {
			return 1;
		}
		DOMNode startNode = this.fullDomDocument.findNodeAt(this.startOffset);
		if (startNode.isOwnerDocument()) {
			return 0;
		}

		DOMNode startNodeParent = startNode.getParentNode();

		if (startNodeParent.isOwnerDocument()) {
			return 0;
		}

		// the starting indent level is the parent's indent level + 1
		int startNodeIndentLevel = getNodeIndentLevel(startNodeParent) + 1;
		return startNodeIndentLevel;
	}

	private int getNodeIndentLevel(DOMNode node) throws BadLocationException {

		Position nodePosition = this.textDocument.positionAt(node.getStart());
		String textBeforeNode = this.textDocument.lineText(nodePosition.getLine()).substring(0,
				nodePosition.getCharacter() + 1);

		int spaceOrTab = getSpaceOrTabStartOfString(textBeforeNode);

		if (this.sharedSettings.getFormattingSettings().isInsertSpaces()) {
			return (spaceOrTab / this.sharedSettings.getFormattingSettings().getTabSize());
		}
		return spaceOrTab;
	}

	private int getSpaceOrTabStartOfString(String string) {
		int i = 0;
		int spaceOrTab = 0;
		while (i < string.length() && (string.charAt(i) == ' ' || string.charAt(i) == '\t')) {
			spaceOrTab++;
			i++;
		}
		return spaceOrTab;
	}

	private DOMElement getFullDocElemFromRangeElem(DOMElement elemFromRangeDoc) {
		int fullOffset = -1;

		if (elemFromRangeDoc.hasStartTag()) {
			fullOffset = getFullOffsetFromRangeOffset(elemFromRangeDoc.getStartTagOpenOffset()) + 1;
			// +1 because offset must be here: <|root
			// for DOMNode.findNodeAt() to find the correct element
		} else if (elemFromRangeDoc.hasEndTag()) {
			fullOffset = getFullOffsetFromRangeOffset(elemFromRangeDoc.getEndTagOpenOffset()) + 1;
			// +1 because offset must be here: <|/root
			// for DOMNode.findNodeAt() to find the correct element
		} else {
			return null;
		}

		DOMElement elemFromFullDoc = (DOMElement) this.fullDomDocument.findNodeAt(fullOffset);
		return elemFromFullDoc;
	}

	private int getFullOffsetFromRangeOffset(int rangeOffset) {
		return rangeOffset + this.startOffset;
	}

	private boolean startTagExistsInRangeDocument(DOMNode node) {
		if (!node.isElement()) {
			return false;
		}

		return ((DOMElement) node).hasStartTag();
	}

	private boolean startTagExistsInFullDocument(DOMNode node) {
		if (!node.isElement()) {
			return false;
		}

		DOMElement elemFromFullDoc = getFullDocElemFromRangeElem((DOMElement) node);

		if (elemFromFullDoc == null) {
			return false;
		}

		return elemFromFullDoc.hasStartTag();
	}

	private void format(DOMNode node) throws BadLocationException {

		if (linefeedOnNextWrite && (!node.isText() || !((DOMText) node).isWhitespace())) {
			this.xmlBuilder.linefeed();
			linefeedOnNextWrite = false;
		}

		if (node.getNodeType() != DOMNode.DOCUMENT_NODE) {
			boolean doLineFeed = !node.getOwnerDocument().isDTD()
					&& !(node.isComment() && ((DOMComment) node).isCommentSameLineEndTag())
					&& (!node.isText() || (!((DOMText) node).isWhitespace() && ((DOMText) node).hasSiblings()));

			if (this.indentLevel > 0 && doLineFeed) {
				// add new line + indent
				if (!node.isChildOfOwnerDocument() || node.getPreviousNonTextSibling() != null) {
					this.xmlBuilder.linefeed();
				}

				if (!startTagExistsInRangeDocument(node) && startTagExistsInFullDocument(node)) {
					DOMNode startNode = getFullDocElemFromRangeElem((DOMElement) node);
					int currentIndentLevel = getNodeIndentLevel(startNode);
					this.xmlBuilder.indent(currentIndentLevel);
					this.indentLevel = currentIndentLevel;
				} else {
					this.xmlBuilder.indent(this.indentLevel);
				}
			}
			if (node.isElement()) {
				// Format Element
				formatElement((DOMElement) node);
			} else if (node.isCDATA()) {
				// Format CDATA
				formatCDATA((DOMCDATASection) node);
			} else if (node.isComment()) {
				// Format comment
				formatComment((DOMComment) node);
			} else if (node.isProcessingInstruction()) {
				// Format processing instruction
				formatProcessingInstruction(node);
			} else if (node.isProlog()) {
				// Format prolog
				formatProlog(node);
			} else if (node.isText()) {
				// Format Text
				formatText((DOMText) node);
			} else if (node.isDoctype()) {
				// Format document type
				formatDocumentType((DOMDocumentType) node);
			}
		} else if (node.hasChildNodes()) {
			// Other nodes kind like root
			for (DOMNode child : node.getChildren()) {
				format(child);
			}
		}
	}

	/**
	 * Format the given DOM prolog
	 *
	 * @param node the DOM prolog to format.
	 */
	private void formatProlog(DOMNode node) {
		addPrologToXMLBuilder(node, this.xmlBuilder);
		linefeedOnNextWrite = true;
	}

	/**
	 * Format the given DOM text node.
	 *
	 * @param textNode the DOM text node to format.
	 */
	private void formatText(DOMText textNode) {
		String content = textNode.getData();
		if (textNode.equals(this.fullDomDocument.getLastChild())) {
			xmlBuilder.addContent(content);
		} else {
			xmlBuilder.addContent(content, textNode.isWhitespace(), textNode.hasSiblings(), textNode.getDelimiter());
		}
	}

	/**
	 * Format the given DOM document type.
	 *
	 * @param documentType the DOM document type to format.
	 */
	private void formatDocumentType(DOMDocumentType documentType) {
		boolean isDTD = documentType.getOwnerDocument().isDTD();
		if (!isDTD) {
			this.xmlBuilder.startDoctype();
			List<DTDDeclParameter> params = documentType.getParameters();

			for (DTDDeclParameter param : params) {
				if (!documentType.isInternalSubset(param)) {
					xmlBuilder.addParameter(param.getParameter());
				} else {
					xmlBuilder.startDoctypeInternalSubset();
					xmlBuilder.linefeed();
					// level + 1 since the 'level' value is the doctype tag's level
					formatDTD(documentType, this.indentLevel + 1, this.endOffset, this.xmlBuilder);
					xmlBuilder.linefeed();
					xmlBuilder.endDoctypeInternalSubset();
				}
			}
			if (documentType.isClosed()) {
				xmlBuilder.endDoctype();
			}
			linefeedOnNextWrite = true;

		} else {
			formatDTD(documentType, this.indentLevel, this.endOffset, this.xmlBuilder);
		}
	}

	/**
	 * Format the given DOM ProcessingIntsruction.
	 *
	 * @param element the DOM ProcessingIntsruction to format.
	 *
	 */
	private void formatProcessingInstruction(DOMNode node) {
		addPIToXMLBuilder(node, this.xmlBuilder);
		if (this.indentLevel == 0) {
			this.xmlBuilder.linefeed();
		}
	}

	/**
	 * Format the given DOM Comment
	 *
	 * @param element the DOM Comment to format.
	 *
	 */
	private void formatComment(DOMComment comment) {
		this.xmlBuilder.startComment(comment);
		this.xmlBuilder.addContentComment(comment.getData());
		if (comment.isClosed()) {
			// Generate --> only if comment is closed.
			this.xmlBuilder.endComment();
		}
		if (this.indentLevel == 0) {
			linefeedOnNextWrite = true;
		}
	}

	/**
	 * Format the given DOM CDATA
	 *
	 * @param element the DOM CDATA to format.
	 *
	 */
	private void formatCDATA(DOMCDATASection cdata) {
		this.xmlBuilder.startCDATA();
		this.xmlBuilder.addContentCDATA(cdata.getData());
		if (cdata.isClosed()) {
			// Generate ]> only if CDATA is closed.
			this.xmlBuilder.endCDATA();
		}
	}

	/**
	 * Format the given DOM element
	 *
	 * @param element the DOM element to format.
	 *
	 * @throws BadLocationException
	 */
	private void formatElement(DOMElement element) throws BadLocationException {
		String tag = element.getTagName();
		if (element.hasEndTag() && !element.hasStartTag()) {
			// bad element without start tag (ex: <\root>)
			xmlBuilder.endElement(tag, element.isEndTagClosed());
		} else {
			// generate start element
			xmlBuilder.startElement(tag, false);
			if (element.hasAttributes()) {
				formatAttributes(element);
			}

			EmptyElements emptyElements = getEmptyElements(element);
			switch (emptyElements) {
			case expand:
				// expand empty element: <example /> -> <example></example>
				xmlBuilder.closeStartElement();
				// end tag element is done, only if the element is closed
				// the format, doesn't fix the close tag
				this.xmlBuilder.endElement(tag, true);
				break;
			case collapse:
				// collapse empty element: <example></example> -> <example />
				formatElementStartTagSelfCloseBracket(element);
				break;
			default:
				if (element.isStartTagClosed()) {
					formatElementStartTagCloseBracket(element);
				}
				boolean hasElements = false;
				if (element.hasChildNodes()) {
					// element has body

					this.indentLevel++;
					for (DOMNode child : element.getChildren()) {
						hasElements = hasElements || !child.isText();
						format(child);
					}
					this.indentLevel--;
				}
				if (element.hasEndTag()) {
					if (hasElements) {
						this.xmlBuilder.linefeed();
						this.xmlBuilder.indent(this.indentLevel);
					}
					// end tag element is done, only if the element is closed
					// the format, doesn't fix the close tag
					if (element.hasEndTag() && element.getEndTagOpenOffset() <= this.endOffset) {
						this.xmlBuilder.endElement(tag, element.isEndTagClosed());
					} else {
						formatElementStartTagSelfCloseBracket(element);
					}
				} else if (element.isSelfClosed()) {
					formatElementStartTagSelfCloseBracket(element);
				}
			}
		}
	}

	/**
	 * Formats the start tag's closing bracket (>) according to
	 * {@code XMLFormattingOptions#isPreserveAttrLineBreaks()}
	 *
	 * {@code XMLFormattingOptions#isPreserveAttrLineBreaks()}: If true, must add a
	 * newline + indent before the closing bracket if the last attribute of the
	 * element and the closing bracket are in different lines.
	 *
	 * @param element
	 * @throws BadLocationException
	 */
	private void formatElementStartTagCloseBracket(DOMElement element) throws BadLocationException {
		if (this.sharedSettings.getFormattingSettings().isPreserveAttributeLineBreaks() && element.hasAttributes()
				&& !isSameLine(getLastAttribute(element).getEnd(), element.getStartTagCloseOffset())) {
			xmlBuilder.linefeed();
			this.xmlBuilder.indent(this.indentLevel);
		}
		xmlBuilder.closeStartElement();
	}

	/**
	 * Formats the self-closing tag (/>) according to
	 * {@code XMLFormattingOptions#isPreserveAttrLineBreaks()}
	 *
	 * {@code XMLFormattingOptions#isPreserveAttrLineBreaks()}: If true, must add a
	 * newline + indent before the self-closing tag if the last attribute of the
	 * element and the closing bracket are in different lines.
	 *
	 * @param element
	 * @throws BadLocationException
	 */
	private void formatElementStartTagSelfCloseBracket(DOMElement element) throws BadLocationException {
		if (this.sharedSettings.getFormattingSettings().isPreserveAttributeLineBreaks() && element.hasAttributes()) {
			int elementEndOffset = element.getEnd();
			if (element.isStartTagClosed()) {
				elementEndOffset = element.getStartTagCloseOffset();
			}
			if (!isSameLine(getLastAttribute(element).getEnd(), elementEndOffset)) {
				this.xmlBuilder.linefeed();
				this.xmlBuilder.indent(this.indentLevel);
			}
		}

		this.xmlBuilder.selfCloseElement();
	}

	private void formatAttributes(DOMElement element) throws BadLocationException {
		List<DOMAttr> attributes = element.getAttributeNodes();
		boolean isSingleAttribute = hasSingleAttributeInFullDoc(element);
		int prevOffset = element.getStart();
		for (DOMAttr attr : attributes) {
			formatAttribute(attr, isSingleAttribute, prevOffset);
			prevOffset = attr.getEnd();
		}
		if ((this.sharedSettings.getFormattingSettings().getClosingBracketNewLine()
				&& this.sharedSettings.getFormattingSettings().getSplitAttributes() == SplitAttributes.splitNewLine) && !isSingleAttribute) {
			xmlBuilder.linefeed();
			// Indent by tag + splitAttributesIndentSize to match with attribute indent
			// level
			int totalIndent = this.indentLevel
					+ this.sharedSettings.getFormattingSettings().getSplitAttributesIndentSize();
			xmlBuilder.indent(totalIndent);
		}
	}

	private void formatAttribute(DOMAttr attr, boolean isSingleAttribute, int prevOffset) throws BadLocationException {
		if (this.sharedSettings.getFormattingSettings().isPreserveAttributeLineBreaks()
				&& !isSameLine(prevOffset, attr.getStart())) {
			xmlBuilder.linefeed();
			xmlBuilder.indent(this.indentLevel + 1);
			xmlBuilder.addSingleAttribute(attr, false, false);
		} else if (isSingleAttribute) {
			xmlBuilder.addSingleAttribute(attr);
		} else {
			xmlBuilder.addAttribute(attr, this.indentLevel);
		}
	}

	/**
	 * Returns true if first offset and second offset belong in the same line of the
	 * document
	 *
	 * If current formatting is range formatting, the provided offsets must be
	 * ranged offsets (offsets relative to the formatting range)
	 *
	 * @param first  the first offset
	 * @param second the second offset
	 * @return true if first offset and second offset belong in the same line of the
	 *         document
	 * @throws BadLocationException
	 */
	private boolean isSameLine(int first, int second) throws BadLocationException {
		if (isRangeFormatting()) {
			// adjust range offsets so that they are relative to the full document
			first = getFullOffsetFromRangeOffset(first);
			second = getFullOffsetFromRangeOffset(second);
		}
		return getLineNumber(first) == getLineNumber(second);
	}

	private int getLineNumber(int offset) throws BadLocationException {
		return this.textDocument.positionAt(offset).getLine();
	}

	private DOMAttr getLastAttribute(DOMElement element) {
		if (!element.hasAttributes()) {
			return null;
		}
		List<DOMAttr> attributes = element.getAttributeNodes();
		return attributes.get(attributes.size() - 1);
	}

	/**
	 * Returns true if the provided element has one attribute in the fullDomDocument
	 * (not the rangeDomDocument)
	 *
	 * @param element
	 * @return true if the provided element has one attribute in the fullDomDocument
	 *         (not the rangeDomDocument)
	 */
	private boolean hasSingleAttributeInFullDoc(DOMElement element) {
		DOMElement fullElement = getFullDocElemFromRangeElem(element);
		return fullElement.getAttributeNodes().size() == 1;
	}

	/**
	 * Return the option to use to generate empty elements.
	 *
	 * @param element the DOM element
	 * @return the option to use to generate empty elements.
	 */
	private EmptyElements getEmptyElements(DOMElement element) {
		if (this.emptyElements != EmptyElements.ignore) {
			if (element.isClosed() && element.isEmpty()) {
				// Element is empty and closed
				switch (this.emptyElements) {
				case expand:
				case collapse: {
					if (this.sharedSettings.getFormattingSettings().isPreserveEmptyContent()) {
						// preserve content
						if (element.hasChildNodes()) {
							// The element is empty and contains somes spaces which must be preserved
							return EmptyElements.ignore;
						}
					}
					return this.emptyElements;
				}
				default:
					return this.emptyElements;
				}
			}
		}
		return EmptyElements.ignore;
	}

	private static boolean formatDTD(DOMDocumentType doctype, int level, int end, XMLBuilder xmlBuilder) {
		DOMNode previous = null;
		for (DOMNode node : doctype.getChildren()) {
			if (previous != null) {
				xmlBuilder.linefeed();
			}

			xmlBuilder.indent(level);

			if (node.isText()) {
				xmlBuilder.addContent(((DOMText) node).getData().trim());
			} else if (node.isComment()) {
				DOMComment comment = (DOMComment) node;
				xmlBuilder.startComment(comment);
				xmlBuilder.addContentComment(comment.getData());
				xmlBuilder.endComment();
			} else if (node.isProcessingInstruction()) {
				addPIToXMLBuilder(node, xmlBuilder);
			} else if (node.isProlog()) {
				addPrologToXMLBuilder(node, xmlBuilder);
			} else {
				boolean setEndBracketOnNewLine = false;
				DTDDeclNode decl = (DTDDeclNode) node;
				xmlBuilder.addDeclTagStart(decl);

				if (decl.isDTDAttListDecl()) {
					DTDAttlistDecl attlist = (DTDAttlistDecl) decl;
					List<DTDAttlistDecl> internalDecls = attlist.getInternalChildren();

					if (internalDecls == null) {
						for (DTDDeclParameter param : decl.getParameters()) {
							xmlBuilder.addParameter(param.getParameter());
						}
					} else {
						boolean multipleInternalAttlistDecls = false;
						List<DTDDeclParameter> params = attlist.getParameters();
						DTDDeclParameter param;
						for (int i = 0; i < params.size(); i++) {
							param = params.get(i);
							if (attlist.getNameParameter().equals(param)) {
								xmlBuilder.addParameter(param.getParameter());
								if (attlist.getParameters().size() > 1) { // has parameters after elementName
									xmlBuilder.linefeed();
									xmlBuilder.indent(level + 1);
									setEndBracketOnNewLine = true;
									multipleInternalAttlistDecls = true;
								}
							} else {
								if (multipleInternalAttlistDecls && i == 1) {
									xmlBuilder.addUnindentedParameter(param.getParameter());
								} else {
									xmlBuilder.addParameter(param.getParameter());
								}
							}
						}

						for (DTDAttlistDecl attlistDecl : internalDecls) {
							xmlBuilder.linefeed();
							xmlBuilder.indent(level + 1);
							params = attlistDecl.getParameters();
							for (int i = 0; i < params.size(); i++) {
								param = params.get(i);

								if (i == 0) {
									xmlBuilder.addUnindentedParameter(param.getParameter());
								} else {
									xmlBuilder.addParameter(param.getParameter());
								}
							}
						}
					}
				} else {
					for (DTDDeclParameter param : decl.getParameters()) {
						xmlBuilder.addParameter(param.getParameter());
					}
				}
				if (setEndBracketOnNewLine) {
					xmlBuilder.linefeed();
					xmlBuilder.indent(level);
				}
				if (decl.isClosed()) {
					xmlBuilder.closeStartElement();
				}
			}
			previous = node;
		}
		return true;
	}

	private List<? extends TextEdit> getFormatTextEdit() throws BadLocationException {
		Position startPosition = this.textDocument.positionAt(this.startOffset);
		Position endPosition = this.textDocument.positionAt(this.endOffset);
		Range r = new Range(startPosition, endPosition);
		List<TextEdit> edits = new ArrayList<>();

		// check if format range reaches the end of the document
		if (this.endOffset == this.textDocument.getText().length()) {

			if (this.sharedSettings.getFormattingSettings().isTrimFinalNewlines()) {
				this.xmlBuilder.trimFinalNewlines();
			}

			if (this.sharedSettings.getFormattingSettings().isInsertFinalNewline()
					&& !this.xmlBuilder.isLastLineEmptyOrWhitespace()) {
				this.xmlBuilder.linefeed();
			}
		}

		edits.add(new TextEdit(r, this.xmlBuilder.toString()));
		return edits;
	}

	private static void addPIToXMLBuilder(DOMNode node, XMLBuilder xml) {
		DOMProcessingInstruction processingInstruction = (DOMProcessingInstruction) node;
		xml.startPrologOrPI(processingInstruction.getTarget());

		String content = processingInstruction.getData();
		if (content.length() > 0) {
			xml.addContentPI(content);
		} else {
			xml.addContent(" ");
		}

		xml.endPrologOrPI();
	}

	private static void addPrologToXMLBuilder(DOMNode node, XMLBuilder xml) {
		DOMProcessingInstruction processingInstruction = (DOMProcessingInstruction) node;
		xml.startPrologOrPI(processingInstruction.getTarget());
		if (node.hasAttributes()) {
			addPrologAttributes(node, xml);
		}
		xml.endPrologOrPI();
	}

	/**
	 * Will add all attributes, to the given builder, on a single line
	 */
	private static void addPrologAttributes(DOMNode node, XMLBuilder xmlBuilder) {
		List<DOMAttr> attrs = node.getAttributeNodes();
		if (attrs == null) {
			return;
		}
		for (DOMAttr attr : attrs) {
			xmlBuilder.addPrologAttribute(attr);
		}
	}
}